Logo notonlyowner

Permissions of a permissionless staking pool


Article cover

Rocket Pool is far more interesting than other smart-contract-based staking pools.

However, to what extent can we consider it a permissionless staking pool?

The closest I found to an answer was this high-level article outlining some of the roles in the system. But it could be outdated (it was published in February 2021) and I wanted a more technical explanation.

So I began digging into Rocket Pool’s contracts. Documenting roles, permissions and powers as I was finding them today on Ethereum mainnet.

What started as a personal list of random notes has matured into this article.

Perhaps this can serve as a starting point for the Rocket Pool community to build and publish “official” technical documentation stating the roles in the system, along with the associated powers, risks and expected behaviors.

The trusted nodes

Trusted nodes are a set of accounts with significant powers and responsibilities in Rocket Pool. They play a critical role to keep it healthy and safe. In return for their services, the system pays them in RPL tokens.

At the moment of writing there are 14 of these trusted nodes. They are all controlled by EOAs. Their addresses, along with public Etherscan tags, are:

0xB3A533098485bede3Cb7fA8711AF84FE0bb1e0aD - Nimbus Oracle DAO
0xCCbFF44E0f0329527feB0167bC8744d7D5aEd3e9 - Rocket Pool: Oracle DAO Node
0xAf820bb236FdE6f084641c74a4C62fA61D10b293 - Etherscan: Oracle DAO Node
0x8d074ad69b55dD57dA2708306f1c200ae1803359 - Rocket Pool: Oracle DAO Node4
0x751683968FD078341C48B90bC657d6bAbc2339F7 - EthStaker: Oracle DAO Node
0x1Df9a4f9421ae6306BC7b171Dff8470640f96e72 - CryptoManufaktur: Oracle DAO Node
0xB13fA6Eff52e6Db8e9F0f1B60B744A9a9A01425A - Blockchain Capital: Oracle DAO node
0x2c6c5809A257ea74a2Df6d20aeE6119196d4bEA0 - Bankless DAO: Oracle DAO Node
0xD7F94c53691AFB5A616C6af96e7075c1FFA1D8eE - beaconcha.in: Oracle DAO Node
0xDf590A63B91E8278a9102BEe9aAfd444F8A4b780 - Consensys: Oracle DAO Node
0x9c69e7FCe961FD837F99c808aD75ED39a2c549Cb - Fire Eyes DAO: Oracle DAO Node
0xc5D291607600044348E5014404cc18394BD1D57d - Lighthouse: Oracle DAO Node
0x2354628919e1D53D2a69cF700Cc53C4093977B94 - no tag
0x16222268bB682AA34cE60C73F4527F30aCA1b788 - no tag

No surprise there are familiar names in the list. These are already mentioned in Rocket Pool’s landing page.

These accounts operate by running an instance of the Rocket Pool Smart Node. The Golang code of Smart Nodes is open source and available at this repository.

Identifying trusted nodes operations

Permissioned functions that only trusted nodes must call are commonly marked with the onlyTrustedNode modifier. The modifier is defined in the RocketBase contract.

Though sparsely used, another way of authorizing trusted nodes is using the getMemberIsValid function of the RocketDAONodeTrusted contract (deployed here).

With these two authorization mechanisms in mind, one can start mapping most (if not all) of these privileged accounts’ powers. It’s a matter of looking for exposed contract functions that at some point authorize the caller (msg.sender) using either of them.

Permissioned operations

The set of permissioned operations delegated to trusted nodes can be divided into: management of trusted proposals and actions, submission of network balances, submission of network prices, submission of rewards snapshots, submission of penalties, and minipool management.

Trusted proposals

Trusted nodes are the sole actors of Rocket Pool with the power to submit, vote and cancel proposals on the Oracle DAO (sometimes referred to as oDAO for short). Following a simple voting system, they can reach consensus to change sensitive parameters and contracts of the system.

The main entrypoint for proposals is the RocketDAONodeTrustedProposals contract (deployed here). This contract is associated with a RocketDAOProposal contract (deployed here). The latter handles the lower-level reading/writing operations to effectively manage proposals throughout their whole lifecycle.

There are 3 main operations that trusted nodes can perform on the RocketDAONodeTrustedProposals contract: creation, voting and cancelling of proposals.

Execution is permissionless, and done via the contract’s execute function. At the moment of writing, a proposal can only be executed after >50% of the trusted nodes have explicitly voted in favor. In numbers: 8 out of 14.

Creating proposals

Done using the propose function.

The amount of proposals that a trusted node can submit in a given window of time is limited.

A proposal includes arbitrary calldata intended to be executed on the RocketDAONodeTrustedProposals contract itself, once the proposal in considered approved.

Therefore, in practice, actions are just calls to functions of the RocketDAONodeTrustedProposals contract. These functions, only available to be called via proposals, can be found marked with the onlyExecutingContracts modifier. They include:

Voting proposals

Done using the vote function.

Voting is the way to express support for a submitted proposal that attempts to execute one of the available actions. Each trusted node can cast one vote per proposal.

Voting is allowed as long as is done within a determined time window. Moreover, the voter must have joined the group of trusted nodes prior to the proposal being created.

Cancelling proposals

Done using the cancel function.

Only the creator of a proposal can cancel it. Cancelling is only possible if the proposal is pending (voting period hasn’t started) or active (voting period has started but not finished).

Trusted actions

In some cases, an approved proposal requires an additional action from a trusted node. These actions are included in the RocketDAONodeTrustedActions contract. The most relevant ones are:

Furthermore, any trusted node can freely challenge another trusted node. Non-trusted nodes can do it too, but they must pay ETH (so as to prevent spamming). Challenging is done calling the actionChallengeMake function. If the challenged account does not respond timely to the challenge, they may be removed by any registered node that calls the actionChallengeDecide function.

Network balances

Rocket Pool requires externally provided balance information to operate efficiently. This information is provided by the trusted nodes, this time acting as oracles for Rocket Pool.

Trusted nodes / oracles are expected to periodically call the submitBalances function of the RocketNetworkBalances contract (deployed here) to report:

In the screenshot below you can see 8 of the trusted nodes submitting the network balances:

Notably, no single actor is entitled to act as the ultimate source of truth for these balances. Instead, network balances are only updated on-chain once a consensus threshold is met. That is, once a number of trusted nodes call the submitBalances function with the same information.

The current consensus threshold required is >50% of the trusted nodes. The threshold value can be queried from the RocketDAOProtocolSettingsNetwork contract (deployed here).

Trusted nodes cannot change this threshold. Instead, it is controlled by a guardian account (more on this later).

Network prices

Rocket Pool also needs the current market price of RPL. For example, to determine the minimum and maximum amount of RPL tokens nodes must stake as collateral.

The price of RPL, along with how much RPL is effectively staked, must be submitted periodically by the trusted nodes. The process is similar to the submission of network balances.

Trusted nodes call the submitPrices function of the RocketNetworkPrices contract (deployed here) to report:

In the screenshot below you can see 8 of the trusted nodes submitting prices:

The RPL price is fetched by each individual trusted node from an on-chain contract. More specifically, their Smart Nodes regularly query a smart contract from 1inch. Then simply submit the price to the Rocket Pool contract.

See this by yourself in the Smart Node service’s code. Here the RPL price is fetched from the 1inch contract at mainnet address 0x07D91f5fb9Bf7798734C3f606dB065549F6893bb, and a few lines later the price is submitted to the Rocket Pool contract.

Going deeper into how the 1inch oracle builds the price for RPL in ETH, you might notice that the actual price is the result of combining the spot price of RPL in two Uniswap V3 pools of RPL/WETH (this pool and this pool).

Rewards

All regular node operators in Rocket Pool are entitled to frequent rewards of RPL tokens. Reward mechanics are explained in the docs.

The assignment of rewards requires snapshots of on-chain state to be taken regularly. That’s also done by trusted nodes. They make these snapshots available on-chain calling the submitRewardSnapshot function of the RocketRewardsPool contract (deployed here).

In the screenshot below you can see 8 of the trusted nodes submitting the rewards snapshots.

While trusted nodes are the only ones who can submit snapshots, in principle there shouldn’t be anyone with enough power to maliciously influence the submitted data. It’s similar to network balances and prices. >50% of the trusted nodes must agree on the same snapshot before it becomes actionable and enables distribution of RPL.

Penalties

Trusted nodes are entitled to penalize regular node operators. They can do it by calling the submitPenalty function of the RocketNetworkPenalties contract (deployed here). Trusted nodes must provide:

More than 50% of the trusted nodes must agree on a penalty for a minipool before its penalty counter is incremented by one. One the counter is 3 or more, the minipool starts accounting penalty rates.

For more information on the penalty system and what kind of actions trusted nodes must consider a penalty, refer to the docs.

Minipool management

Regarding minipool management, there are two main operations delegated to trusted nodes.

First operation is “scrubbing” a minipool in prelaunch state. According to the docs, trusted nodes should scrub a minipool when its withdrawal credentials are not set correctly. Scrubbing leads to dissolving the minipool, and assigning penalties to the node operator (if these are enabled). It’s done calling the voteScrub function of RocketMinipoolDelegate contract, via the corresponding instance of a RocketPool contract (that will delegatecall to RocketMinipoolDelegate). Scrubbing has its own quorum setting, currently set to 51%. This value can be queried using the getScrubQuorum function of the RocketDAONodeTrustedSettingsMinipool contract (deployed here).

Second operation is moving staking minipools to the withdrawable state. This is done calling the submitMinipoolWithdrawable function of the RocketMinipoolStatus contract (deployed here). The consensus threshold of >50% must be met for a minipool to be made withdrawable.

The guardian

Aside from the powers granted to the trusted nodes, Rocket Pool intends to delegate other privileges to a broader governance entity steered by RPL token holders (a.k.a. “The Protocol DAO”). This governance body is still in early stages, and hasn’t been fully activated yet.

The main on-chain contract where the Protocol DAO would operate is the RocketDAOProtocol (deployed here). By querying its getBootstrapModeDisabled function, you can verify that the contract is still in "bootstrap mode”.

This means there’s a “guardian” account that holds all powers that Rocket Pool’s Protocol DAO might hold in the future.

The address of the guardian account can be queried using the getGuardian function of the RocketStorage contract (deployed here). It returns 0x0ccf14983364a7735d369879603930afe10df21e at the moment of writing. That’s an EOA.

As long as the RocketDAOProtocol contract is in bootstrap mode, the guardian is entitled to call any function in the contract that is marked with the onlyGuardian modifier (in turn defined in the RocketBase contract).

In short, today the guardian holds absolute powers over several sensitive settings and actions of Rocket Pool.

Let’s see the details.

First, the guardian can arbitrarily change sensitive settings via the RocketDAOProtocol contract, calling its bootstrapSettingMulti, bootstrapSettingUint, bootstrapSettingBool, bootstrapSettingAddress and bootstrapSettingClaimer functions. The specific settings are split into individual contracts for ease of reference.

Second, the guardian can set the maximum penalty rate for all minipools in Rocket Pool. Setting the maximum rate to zero would effective disable penalties. Done calling the setMaxPenaltyRate function of the RocketMinipoolPenalty contract (deployed here).

Third, the guardian can spend RPL tokens from the treasury. Done calling the bootstrapSpendTreasury function of the RocketDAOProtocol contract.

Last, the guardian can resign its powers by disabling the bootstrap mode. This is done calling the bootstrapDisable function of the RocketDAOProtocol contract. An action that cannot be undone.

The guardian used to have similar ruling powers on the RocketDAONodeTrusted contract, which ruled the RocketDAONodeTrustedProposals contract. At some point, these power were passed to the set of trusted nodes. That’s why today the RocketDAONodeTrusted contract is no longer in bootstrap mode.

Once Rocket Pool’s Protocol DAO matures, we might see the guardian abdicating its powers on the RocketDAOProtocol contract, which today rules the RocketDAOProtocolProposals contract. Finally handing over the reins to a larger community interested in the well-being of the protocol.

Refer to this article for more details on Rocket Pool's governance roadmap.